﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.ComponentModel;

namespace Noris.Schedule.Support.Parser
{
    /// <summary>
    /// Panel, který zapouzdřuje editor textu spojený s Parserem se zvýrazněním syntaxe (za pomoci RTF editoru).
    /// Editor má dvě zásadní property: ParserSetting (sem se vkládají pravidla pro parsování), EditedText (vstup / výstup textu), a událost EditedTextChanged (vyvolá se po každé změně textu).
    /// 
    /// </summary>
    public class Editor : Panel
    {
        #region Konstrukce, proměnné
        /// <summary>
        /// Vytvoří Editor
        /// </summary>
        public Editor()
        {
            this.InitComponents();
        }
        private void InitComponents()
        {
            this.DoubleBuffered = true;

            this.RtfEditor = new EditorTextBox();
            this.RtfEditor.Dock = DockStyle.Fill;
            this.RtfEditor.TextChanged += new EventHandler(this._RtfEditor_TextChanged);
            this.Controls.Add(this.RtfEditor);

            this.GotFocus += new EventHandler(_Editor_GotFocus);
        }
        void _Editor_GotFocus(object sender, EventArgs e)
        {
            this.RtfEditor.Focus();
        }
        /// <summary>
        /// Objekt RTF editoru
        /// </summary>
        private EditorTextBox RtfEditor;
        /// <summary>
        /// Úložiště pro property ParserSetting
        /// </summary>
        private ParserSetting _ParserSetting;
        #endregion
        #region Public property, eventy, metody
        /// <summary>
        /// Uložený objekt settingu, který slouží k parsování zadaného textu a k detekci jeho segmentů a klíčových slov.
        /// Výchozí setting je ParserDefaultSetting.MsSql.
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public ParserSetting ParserSetting
        {
            get
            {
                if (this._ParserSetting == null)
                    this._ParserSetting = ParserDefaultSetting.MsSql;  // ParserDefaultSetting.MsSql
                return this._ParserSetting;
            }
            set
            {
                this._ParserSetting = value;
                this.RtfTextParseAndColor();
            }
        }
        /// <summary>
        /// Editovaný text, celý.
        /// Po vložení není vložený text označen.
        /// Vyvolá se event o změně (this.EditedTextChanged).
        /// Pokud se text změní z eventu this.EditedTextChanged, nezpůsobí to nové vyvolání tohoto eventu (rekurzivní), Control tomu sám zabrání.
        /// Nicméně ostatní akce po změně textu proběhnou = nový text je syntakticky podbarvem a parsované segmenty jsou uloženy do this.EditorSegments.
        /// </summary>
        [Browsable(true)]
        [Category("Editor")]
        [Description("Editovaný text")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public string EditedText
        {
            get { return this.RtfEditor.Text; }
            set { this.RtfEditor.Text = value; }
        }
        /// <summary>
        /// Editovaný text, aktuálně označená část (lze číst i vkládat).
        /// Po vložení textu je původní text přepsán (ztratí se) a vložený text je označen (Selected).
        /// Vyvolá se event o změně (this.EditedTextChanged).
        /// Pokud se text změní z eventu this.EditedTextChanged, nezpůsobí to nové vyvolání tohoto eventu (rekurzivní), Control tomu sám zabrání.
        /// Nicméně ostatní akce po změně textu proběhnou = nový text je syntakticky podbarvem a parsované segmenty jsou uloženy do this.EditorSegments.
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public string SelectedText
        {
            get { return this.RtfEditor.SelectedText; }
            set
            {
                int selStart = this.RtfEditor.SelectionStart;
                this.RtfEditor.SelectedText = value;
                this.RtfEditor.SelectionStart = selStart;
                this.RtfEditor.SelectionLength = value.Length;
            }
        }
        /// <summary>
        /// Aktuální pozice kurzoru v textu
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int SelectionStart
        {
            get { return this.RtfEditor.SelectionStart; }
            set { this.RtfEditor.SelectionStart = value; }
        }
        /// <summary>
        /// Aktuální délka výběru (označený text) v textu
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public int SelectionLength
        {
            get { return this.RtfEditor.SelectionLength; }
            set { this.RtfEditor.SelectionLength = value; }
        }
        /// <summary>
        /// Vybere (označí) daný blok textu, daný jeho pozicí.
        /// </summary>
        /// <param name="selectionStart"></param>
        /// <param name="selectionLength"></param>
        public void SelectRange(int selectionStart, int selectionLength)
        {
            this.RtfEditor.SelectionStart = selectionStart;
            this.RtfEditor.SelectionLength = selectionLength;
        }
        /// <summary>
        /// Povolení editace textu
        /// </summary>
        [Browsable(true)]
        [Category("Editor")]
        [Description("Povolení editace textu")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public bool EditorEnabled
        {
            get { return !this.RtfEditor.ReadOnly; }
            set
            {
                this.RtfEditor.ReadOnly = !value;
                this.RtfEditor.BackColor = (value ? SystemColors.ControlLightLight : SystemColors.ControlLight);
            }
        }
        /// <summary>
        /// Reference na editor pro další přímé zásahy.
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public EditorTextBox RtfTextBox { get { return this.RtfEditor; } }
        /// <summary>
        /// Parsované segmenty aktuálního textu. Po každé změně textu jsou aktuální.
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public IEnumerable<ParserSegment> EditorSegments { get; private set; }
        /// <summary>
        /// Událost po změně textu SQL příkazu, proběhne po dokončení parsování.
        /// Pokud se v tomto eventu provede změna textu, (což by vyvolalo rekurzi), provede se další parsování textu, aktualizace pole segmentů this.EditorSegments,
        /// ale již se nevyvolá (rekurzivně) tento event EditedTextChanged.
        /// Tedy control sám aktivně brání zacyklení eventu.
        /// </summary>
        [Browsable(true)]
        [Category("SqlEditor")]
        [Description("Událost po změně textu SQL příkazu, proběhne po dokončení parsování")]
        public event EventHandler EditedTextChanged;
        #endregion
        #region Editace RTF textu, parsování, syntax coloring
        /// <summary>
        /// Změnila se hodnota textu v editoru. Mělo by proběhnout parsování a syntax coloring.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _RtfEditor_TextChanged(object sender, EventArgs e)
        {
            bool runEvent = false;
            if (!this._SuppressRtfEditorTextChanged)
            {
                string currentText = this.RtfEditor.Text;
                if (this._IsTextChanged(currentText))
                {
                    try
                    {
                        this._SuppressRtfEditorTextChanged = true;
                        if (currentText.Length < 1000)
                            this.RtfTextParseAndColor();
                        else
                            this._TimerToParsesStart();
                        runEvent = (this.EditedTextChanged != null);
                    }
                    finally
                    {
                        this._SuppressRtfEditorTextChanged = false;
                    }
                }
            }

            // Toto řešení, kdy vyvolání eventu EditedTextChanged je umístěno až za blok if (!this._SuppressRtfEditorTextChanged) umožňuje následující chování:
            //  a) Uživatel změní text => proběhne jeho parsování a SyntaxColoring (v bloku _SuppressRtfEditorTextChanged, takže se interně nezacyklí)
            //  b) Probíhající SyntaxColoring změní RTF text, ale uvnitř bloku _SuppressRtfEditorTextChanged, takže rekurzivní SyntaxColoring neproběhne, 
            //     a proměnná runEvent v druhé iteraci této metody _RtfEditor_TextChanged() zůstane false (druhá iterace neprovede nic)
            //  c) Po provedení SyntaxColoring (metoda RtfTextParseAndColor()) se nastaví runEvent na true (pokud je zadán delegát), 
            //     a protože implicitně není event potlačen (_SuppressEventEditedTextChanged), provede se event - ale po jeho dobu se potlačí jeho rekurze pomocí _SuppressEventEditedTextChanged.
            //  d) Nyní může eventhandler klidně změnit text (pokud například najde chyby), tím dojde k další iteraci této metody _RtfEditor_TextChanged(), 
            //     přičemž není potlačen SyntaxColoring => provede se (změna textu se barevně projeví),
            //     ale je potlačeno volání externího eventu (_SuppressEventEditedTextChanged je true), takže změna textu, kterou provede uživatel zevnitř svého eventu jej nevyvolá rekurzivně !!!
            //  Pokud ale uživatel změní text odjinud než ze svého eventu, pak se jeho event vyvolá (protože není nastaven _SuppressEventEditedTextChanged na true).
            if (runEvent && !this._SuppressEventEditedTextChanged)
            {   // Pokud se má volat externí event, a není potlačeno jeho volání:
                try
                {
                    this._SuppressEventEditedTextChanged = true;
                    this.EditedTextChanged(this, EventArgs.Empty);
                }
                finally
                {
                    this._SuppressEventEditedTextChanged = false;
                }
            }
        }
        /// <summary>
        /// Zjistí, zda daný text je jiný, než který se posledně parsoval
        /// </summary>
        /// <param name="currentText"></param>
        /// <returns></returns>
        private bool _IsTextChanged(string currentText)
        {
            if (currentText == null) return false;
            if (this._LastParsedText == null) return true;
            return !String.Equals(currentText, this._LastParsedText);
        }
        private string _LastParsedText = null;
        /// <summary>
        /// Zajistí syntaktické zpracování textu v RTF editoru
        /// </summary>
        private void RtfTextParseAndColor()
        {
            int visStart = this.RtfEditor.GetCharIndexFromPosition(new Point(2, 5));        // Index znaku, který je první viditelný vlevo nahoře v editoru
            int selStart = this.RtfEditor.SelectionStart;
            int selLength = this.RtfEditor.SelectionLength;
            string text = this.RtfEditor.Text;

            // RTF control do property Text vkládá konec řádku jako znak 0x0a (jen LF).
            // Parseru to nevadí, ale následně obsahuje taky jen LF a ne CR, a controly TextBox to nezobrazí dobře.
            // Proto LF rozšířím na CrLf:
            text = text.Replace("\n", "\r\n");

            string rtfText = RtfTextGetSyntaxColor(text);
            if (rtfText != null)
            {
                // this.RtfEditor.SuspendLayout();
                this.RtfEditor.WmMsg = "";
                this.RtfEditor.SuspendPaint = true;
                this.RtfEditor.Rtf = rtfText;
                this.RtfEditor.SelectionStart = visStart;
                this.RtfEditor.ScrollToCaret();
                this.RtfEditor.SelectionStart = selStart;
                if (selLength > 0)
                    this.RtfEditor.SelectionLength = selLength;
                this.RtfEditor.SuspendPaint = false;
                // this.RtfEditor.ResumeLayout(false);
                // System.Windows.Forms.Clipboard.SetText(this.RtfEditor.WmMsg);
            }

            this._LastParsedText = text;
        }
        /// <summary>
        /// Vrátí RTF text obsahující syntakticky zpracovaný vstupující holý text
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        private string RtfTextGetSyntaxColor(string text)
        {
            ParserSetting setting = this.ParserSetting;

            List<ParserSegment> segments = Parser.ParseString(text, setting);
            this.EditorSegments = segments;
            if (segments == null || segments.Count == 0) return null;
            return segments[0].RtfText;
        }
        /// <summary>
        /// true potlačí event _RtfEditor_TextChanged a tedy i EditedTextChanged.
        /// </summary>
        private bool _SuppressRtfEditorTextChanged;
        /// <summary>
        /// true potlačí externí event EditedTextChanged, ale ne interní _RtfEditor_TextChanged
        /// </summary>
        private bool _SuppressEventEditedTextChanged;
        #endregion
        #region Opožděný SyntaxColoring, pokud je text příliš dlouhý
        /// <summary>
        /// Zajistí, že za 650 milisekund od teď se provede metoda this.RtfTextParseAndColor().
        /// Pokud mezitím bude opět volána tato metoda, odloží se její start na 650 milisekund od příštího volání zdejší metody.
        /// Metoda this.RtfTextParseAndColor() tedy bude volána až po 650ms klidu.
        /// </summary>
        private void _TimerToParsesStart()
        {
            if (_Timer == null)
            {
                _Timer = new Timer();
                _Timer.Tick += new EventHandler(_Timer_Tick);
            }
            _Timer.Stop();
            _Timer.Interval = 650;
            _Timer.Start();
        }
        /// <summary>
        /// Uplynul potřebný čas.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void _Timer_Tick(object sender, EventArgs e)
        {
            _Timer.Stop();

            bool suppressInternalEvent = this._SuppressRtfEditorTextChanged;
            bool suppressExternalEvent = this._SuppressEventEditedTextChanged;
            try
            {
                this._SuppressRtfEditorTextChanged = true;
                this._SuppressEventEditedTextChanged = true;
                if (this.InvokeRequired)
                    this.BeginInvoke(new Action(this.RtfTextParseAndColor));
                else
                    this.RtfTextParseAndColor();
            }
            finally
            {
                this._SuppressRtfEditorTextChanged = suppressInternalEvent;
                this._SuppressEventEditedTextChanged = suppressExternalEvent;
            }
        }
        private Timer _Timer;
        #endregion
    }
    #region class EditorTextBox : RTF editor (RichTextBox)
    /// <summary>
    /// EditorTextBox : RTF editor (RichTextBox)
    /// </summary>
    public class EditorTextBox : RichTextBox
    {
        public EditorTextBox()
        {
            this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            // Po dobu, kdy je nastaveno this.SuspendPaint se pokusím pozastavit vykreslování, aby okno s textboxem příliš neblikalo:
            // mimochodem, sem to nikdy nechodí :-) 
            if (!SuspendPaint)
                base.OnPaint(e);
        }
        protected override void WndProc(ref Message m)
        {
            // Za stavu Suspend si střádám všechny message kvůli debugu:
            if (this.SuspendPaint)
                this.WmMsg += m.Msg.ToString("X4") + "; ";

            // Po dobu, kdy je nastaveno this.SuspendPaint se pokusím pozastavit vykreslování, aby okno s textboxem příliš neblikalo:
            // RTF chodí jen tudy!
            if (!(this.SuspendPaint && (m.Msg == WM_PAINT)))
                base.WndProc(ref m);
        }
        public bool SuspendPaint { get; set; }
        public const int WM_GETTEXT = 0x000D;
        public const int WM_PAINT = 0x000F;
        internal string WmMsg = "";
    }
    #endregion
}
